package com.kaithan.framework.common.util;

import cn.hutool.core.io.IoUtil;
import cn.hutool.core.util.StrUtil;
import com.kaithan.framework.common.enums.BaseEnums;
import com.kaithan.framework.common.exception.MyException;
import lombok.extern.slf4j.Slf4j;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.text.PDFTextStripper;
import org.apache.poi.hwpf.HWPFDocument;
import org.apache.poi.hwpf.extractor.WordExtractor;
import org.apache.poi.xwpf.usermodel.XWPFDocument;
import org.apache.poi.xwpf.usermodel.XWPFParagraph;
import org.apache.poi.xwpf.usermodel.XWPFRun;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.util.FileCopyUtils;
import org.springframework.web.multipart.MultipartFile;

import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.DigestInputStream;
import java.security.MessageDigest;
import java.util.Comparator;
import java.util.List;
import java.util.Optional;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import java.util.stream.Stream;


/**
 * 类描述：文件处理工具类
 *
 * @author ZhuYin
 * @since 2024年12月15日
 */
@Slf4j
public class FileUtil extends cn.hutool.core.io.FileUtil {

    private static final String[] hexDigits = {"0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "a", "b", "c", "d", "e", "f"};

    /**
     * 合法的后缀名，后续扩展
     */
    public static String[] ALLOWED_FILE_SUFFIX = new String[]{
            "png", "bmp", "jpg", "jpeg", "pdf",
            "xlsx", "xls", "gif", "svg", "txt",
            "zip", "ppt", "doc", "docx", "html",
            "htm", "ico", "mp3", "mp4", "java",
            "sql", "xml", "js", "py", "php", "vue",
            "sh", "cmd", "py3", "css", "md", "csv",
            "rar", "zip", "json"
    };

    /** 图片文件 */
    public static String[] FILE_SUFFIX_IMAGE = new String[]{"png", "bmp", "jpg", "jpeg", "svg", "gif"};

    /** 代码文件 */
    public static String[] FILE_SUFFIX_CODE = new String[]{"java", "sql", "js", "py", "py3", "php", "vue", "sh", "cmd", "css"};

    /** 常用的office文件 */
    public static String[] FILE_SUFFIX_NORMAL_OFFICE = new String[]{"doc", "docx", "xls", "xlsx", "ppt", "pptx", "pdf", "txt"};

    /**
     * 方法说明：使用windows、非windows都通用的分割符 标准化文件路径
     * 注意：只针对文件系统的路径做处理，网络访问地址暂不考虑
     *
     * @param path 路径
     * @return {@link String } 返回的路径类似为 C:/usr/local 或 /usr/local 这种多系统兼容的形式
     */
    public static String sopPath(String path) {
        if (StringUtils.isBlank(path)) {
            return path;
        }
        // 先将path中的反斜杠换成正斜杠
        path = path.replace("\\", "/");
        // 再将path中所有成对的正斜杠换成单个正斜杠，循环替换，直到path中没有成对的正斜杠为止
        String doubleSlash = "//";
        while (path.contains(doubleSlash)) {
            path = path.replace(doubleSlash, "/");
        }
        return path;
    }

    /**
     * 方法说明：获取文件名，不带后缀
     *
     * @param fileName 带后缀名的文件名
     * @return 返回不带后缀的文件名
     * @author ZhuYin
     * @since 2025年01月06日
     */
    public static String getFileName(String fileName) {
        if (fileName.contains(".")) {
            return fileName.substring(0, fileName.lastIndexOf("."));
        } else {
            return fileName;
        }
    }


    /**
     * 方法说明：获取文件扩展名(不带.号)
     *
     * @param fileName 带后缀名的文件名
     * @return 返回文件后缀(小写)
     * @author ZhuYin
     * @since 2025年01月06日
     */
    public static String getFileExtension(String fileName) {
        if (fileName.contains(".")) {
            return fileName.substring(fileName.lastIndexOf(".") + 1).toLowerCase();
        } else {
            // 无后缀的文件，如 hosts、managed-schema 等文件
            return "";
        }
    }

    /**
     * 方法说明：判断文件类型是否合法
     *
     * @param fileExt 文件扩展名
     * @return boolean
     * @author ZhuYin
     * @since 2025年01月06日
     */
    public static boolean isFileAllowed(String fileExt) {
        if (StringUtils.isBlank(fileExt)) {
            // 没有后缀名的文件也允许上传，如 hosts、managed-schema 等文件
            return true;
        }
        for (String ext : ALLOWED_FILE_SUFFIX) {
            if (ext.equalsIgnoreCase(fileExt)) {
                return true;
            }
        }
        return false;
    }

    /**
     * 方法说明：判断文件是否是代码文件
     *
     * @param fileExt 文件扩展名
     * @return boolean
     * @author ZhuYin
     * @since 2025年01月06日
     */
    public static boolean isCodeFile(String fileExt) {
        for (String ext : FILE_SUFFIX_CODE) {
            if (ext.equalsIgnoreCase(fileExt)) {
                return true;
            }
        }
        return false;
    }

    /**
     * 方法说明：判断文件是否是图片文件
     *
     * @param fileExt 文件扩展名
     * @return boolean
     * @author ZhuYin
     * @since 2025年01月06日
     */
    public static boolean isImgFile(String fileExt) {
        for (String ext : FILE_SUFFIX_IMAGE) {
            if (ext.equalsIgnoreCase(fileExt)) {
                return true;
            }
        }
        return false;
    }

    /**
     * 方法说明：判断文件是否是office文件
     *
     * @param fileExt 文件扩展名
     * @return boolean
     * @author ZhuYin
     * @since 2025年01月06日
     */
    public static boolean isNormalOfficeFile(String fileExt) {
        for (String ext : FILE_SUFFIX_NORMAL_OFFICE) {
            if (ext.equalsIgnoreCase(fileExt)) {
                return true;
            }
        }
        return false;
    }

    /**
     * 方法说明：下载文件
     *
     * @param is         输入流
     * @param outputName 下载时输出给前端的文件名
     * @param response   响应对象
     * @author ZhuYin
     * @since 2025年01月06日
     */
    public static void download(InputStream is, String outputName, HttpServletResponse response) throws IOException {
        ServletOutputStream outputStream = response.getOutputStream();
        // 设置文件ContentType类型，这样设置，会自动判断下载文件类型
        response.setContentType("application/x-msdownload");
        response.setCharacterEncoding(StandardCharsets.UTF_8.name());
        response.setHeader("Content-Disposition", "attachment;fileName=" + URLEncoder.encode(outputName, StandardCharsets.UTF_8.name()));
        IoUtil.copy(is, outputStream);
        response.flushBuffer();
        IoUtil.close(outputStream);
        IoUtil.close(is);
    }

    /**
     * 方法说明：下载resource资源路径下的文件
     *
     * @param resource      ClassPathResource对象
     * @param outputName    输出文件名称
     * @return 返回下载流
     * @author ZhuYin
     * @since 2024年09月27日
     */
    public static ResponseEntity<byte[]> downloadResourceFile(Resource resource, String outputName) throws IOException {
        byte[] bytes = FileCopyUtils.copyToByteArray(resource.getInputStream());
        HttpHeaders headers = new HttpHeaders();
        // 防止文件名乱码问题
        String fileName = new String(outputName.getBytes(StandardCharsets.UTF_8), StandardCharsets.ISO_8859_1);
        headers.setContentType(MediaType.APPLICATION_OCTET_STREAM);
        headers.setContentDispositionFormData("attachment", fileName);
        return new ResponseEntity<>(bytes, headers, HttpStatus.CREATED);
    }

    /**
     * 方法说明：计算文件的md5值
     *
     * @param file 文件对象
     * @return 返回文件的md5值
     * @author ZhuYin
     * @since 2025年01月07日
     */
    public static String fileMd5(File file) throws Exception {
        MessageDigest md = MessageDigest.getInstance("MD5");
        try (InputStream is = Files.newInputStream(file.toPath());
             BufferedInputStream bis = new BufferedInputStream(is)) {
            // 8KB 缓冲区
            byte[] buffer = new byte[8192];
            int bytesRead;
            while ((bytesRead = bis.read(buffer)) != -1) {
                md.update(buffer, 0, bytesRead);
            }
            byte[] digest = md.digest();
            StringBuilder sb = new StringBuilder();
            for (byte b : digest) {
                sb.append(String.format("%02x", b));
            }
            return sb.toString();
        }
    }

    /**
     * 计算MultipartFile的MD5值。
     *
     * @param file MultipartFile对象
     * @return 文件的MD5值
     */
    public static String fileMd5(MultipartFile file) {
        try (InputStream is = file.getInputStream();
             DigestInputStream digestInputStream = new DigestInputStream(is, MessageDigest.getInstance("MD5"))) {
            // 读取流直至结束
            while (digestInputStream.read() != -1) ;
            MessageDigest md = digestInputStream.getMessageDigest();
            byte[] digest = md.digest();
            StringBuilder sb = new StringBuilder();
            for (byte b : digest) {
                sb.append(String.format("%02x", b));
            }
            return sb.toString();
        } catch (Exception e) {
            throw new RuntimeException("计算MD5值时出错", e);
        }
    }

    /**
     * 计算指定目录下以给定前缀开头的文件数量。
     *
     * @param directoryPath 指定的目录路径
     * @param prefix        文件名前缀
     * @return 符合条件的文件数量
     */
    public static int countFilesWithPrefix(String directoryPath, String prefix) {
        Path path = Paths.get(directoryPath);
        if (!Files.exists(path) || !Files.isDirectory(path)) {
            System.out.println("指定的路径不是有效的目录或不存在");
            return 0;
        }

        try (Stream<Path> walk = Files.walk(path)) {
            return Integer.parseInt(walk
                    // 只考虑普通文件
                    .filter(Files::isRegularFile)
                    // 过滤出文件名以指定前缀开头的文件
                    .filter(p -> p.getFileName().toString().startsWith(prefix))
                    // 计算符合条件的文件数量
                    .count() + "");
        } catch (IOException e) {
            log.error("遍历目录时发生错误: " + e.getMessage());
            return -1;
        }
    }

    /**
     * 合并指定目录下以给定前缀开头的分片文件
     *
     * @param directoryPath  目标目录路径
     * @param prefix         文件名前缀
     * @param outputFilePath 输出文件路径
     * @param fileEncrypt    合并后的文件是否要加密
     * @param secretKey      加密密钥
     * @param callback       回调函数，合并成功后回调
     */
    public static boolean mergeChunkFiles(String directoryPath, String prefix, String outputFilePath, boolean fileEncrypt, String secretKey, Consumer<List<Path>> callback) {
        Path path = Paths.get(directoryPath);
        if (!Files.exists(path) || !Files.isDirectory(path)) {
            return false;
        }

        try (Stream<Path> walk = Files.walk(path)) {
            // 将过滤出的文件转换为列表
            List<Path> files = walk.filter(Files::isRegularFile)
                    .filter(p -> p.getFileName().toString().startsWith(prefix))
                    .sorted(getComparator())
                    .collect(Collectors.toList());
            // 创建输出文件
            Path outputPath = Paths.get(outputFilePath);
            // 如果不加密
            try (BufferedOutputStream outputStream = new BufferedOutputStream(Files.newOutputStream(outputPath.toFile().toPath()))) {
                for (Path file : files) {
                    try (BufferedInputStream inputStream = new BufferedInputStream(Files.newInputStream(file.toFile().toPath()))) {
                        byte[] buffer = new byte[1024];
                        int bytesRead;
                        while ((bytesRead = inputStream.read(buffer)) != -1) {
                            outputStream.write(buffer, 0, bytesRead);
                        }
                    }
                }
            }
            if (fileEncrypt) {
                // 加密
                String tempFilePath = outputFilePath + ".temp";
                FileUtil.rename(outputPath.toFile(), tempFilePath, false, true);
                FileEncDecUtil.encrypt(secretKey, new FileInputStream(tempFilePath), new FileOutputStream(outputFilePath));
                // 删除临时文件
                FileUtil.del(tempFilePath);
            }
            if (callback != null) {
                callback.accept(files);
            }
            return true;
        } catch (Exception e) {
            log.error("处理文件时发生错误: {}", e.getMessage(), e);
        }
        return false;
    }

    /**
     * 方法说明：将 MultipartFile 转成 File
     * 转成的文件会保存在系统临时目录下：
     * @param multipartFile 前端上传的 MultipartFile
     * @return {@link File }
     */
    public static File convertFile(MultipartFile multipartFile) {
        return convertFile(multipartFile, null);
    }

    /**
     * 方法说明：将 MultipartFile 转成 File
     *
     * @param multipartFile 前端上传的 MultipartFile
     * @param targetDir     目标目录(不传则使用默认临时目录)
     * @return {@link File }
     */
    public static File convertFile(MultipartFile multipartFile, String targetDir) {
        if (multipartFile == null) {
            return null;
        }
        if (StringUtils.isEmpty(targetDir)) {
            // 使用系统临时目录
            targetDir = System.getProperty("java.io.tmpdir");
        }
        String targetFileAbsolutePath = targetDir + "/" + multipartFile.getOriginalFilename();
        try (InputStream inputStream = multipartFile.getInputStream()) {
            File targetFile = new File(targetFileAbsolutePath);
            if (!targetFile.getParentFile().exists()) {
                targetFile.getParentFile().mkdirs();
            }
            inputStreamToFile(inputStream, targetFile);
            return targetFile;
        } catch (Exception e) {
            log.error("MultipartFile转File异常：", e);
        }
        return null;
    }

    /**
     * 将 File 对象转成 MultipartFile 对象
     * @param file 待转换的File对象
     * @return {@link MultipartFile}
     */
    // public static MultipartFile convertFile(File file) throws Exception {
    //     // File转MultipartFile
    //     FileInputStream fileInputStream = new FileInputStream(file);
    //     return new MockMultipartFile(file.getName(), file.getName(), ContentType.APPLICATION_OCTET_STREAM.toString(), fileInputStream);
    // }

    /**
     * 将输入流转成File
     *
     * @param ins  输入流
     * @param file 目标文件
     */
    public static void inputStreamToFile(InputStream ins, File file) {
        try (OutputStream os = Files.newOutputStream(file.toPath())) {
            int bytesRead;
            byte[] buffer = new byte[2048];
            while ((bytesRead = ins.read(buffer)) != -1) {
                os.write(buffer, 0, bytesRead);
            }
            os.close();
            ins.close();
        } catch (Exception e) {
            log.error("将输入流转成File异常：", e);
        } finally {
            IoUtil.close(ins);
        }
    }

    /**
     * 读取文档内容
     * 目前仅支持 txt, doc, docx, pdf 格式
     *
     * @param file 文件对象
     * @return 文档内容
     */
    public static String readFileContent(MultipartFile file) {
        String filename = file.getOriginalFilename();
        assert filename != null;
        String fileType = filename.substring(filename.lastIndexOf(".") + 1).toUpperCase();
        switch (fileType) {
            case "TXT":
                return readTxtContent(file);
            case "DOC":
            case "DOCX":
                return readWordContent(file);
            case "PDF":
                return readPDFContent(file);
            default:
                return "";
        }
    }

    /**
     * 读取 txt 文档内容
     */
    private static String readTxtContent(MultipartFile file) {
        try {
            return StrUtil.utf8Str(file.getBytes());
        } catch (IOException e) {
            log.error("读取 txt 文件内容时发生 IO 异常:", e);
            return "";
        }
    }

    /**
     * 读取 doc 和 docx 文档内容
     */
    private static String readWordContent(MultipartFile file) {

        String filename = file.getOriginalFilename();
        assert filename != null;
        String fileType = filename.substring(filename.lastIndexOf(".") + 1);
        return "doc".equals(fileType) ? readDocContent(file) : readDocxContent(file);
    }



    /**
     * 读取 .doc 格式的 word 文档
     *
     * @param file 文件对象
     * @return 文本内容
     */
    private static String readDocContent(MultipartFile file) {
        StringBuilder content = new StringBuilder();
        try (InputStream inputStream = file.getInputStream();
             HWPFDocument document = new HWPFDocument(inputStream)) {
            WordExtractor extractor = new WordExtractor(document);
            String[] paragraphs = extractor.getParagraphText();
            for (String paragraph : paragraphs) {
                content.append(paragraph);
            }
        } catch (IOException e) {
            log.error("读取文件内容时发生 IO 异常:", e);
        }
        return content.toString();
    }

    /**
     * 读取 .docx 格式的 word 文档
     *
     * @param file 文件对象
     * @return 文本内容
     */
    private static String readDocxContent(MultipartFile file) {
        StringBuilder content = new StringBuilder();
        try (InputStream inputStream = file.getInputStream();
             XWPFDocument document = new XWPFDocument(inputStream)) {
            for (XWPFParagraph paragraph : document.getParagraphs()) {
                for (XWPFRun run : paragraph.getRuns()) {
                    String runText = run.getText(0);
                    if (runText != null) {
                        content.append(runText);
                    }
                }
            }
        } catch (IOException e) {
            log.error("读取文件内容时发生 IO 异常:", e);
        }
        return content.toString();
    }

    /**
     * 读取 pdf 文档内容
     */
    private static String readPDFContent(MultipartFile file) {
        StringBuilder content = new StringBuilder();
        try (InputStream inputStream = file.getInputStream();
             PDDocument document = PDDocument.load(inputStream)) {
            // 检查是否是由文档转换出的 pdf 文件
            if (!document.isEncrypted() && document.getNumberOfPages() > 0) {
                PDFTextStripper textStripper = new PDFTextStripper();
                content.append(textStripper.getText(document));
            } else {
                log.warn("PDF 已加密或不是由文档转换的 PDF 格式，无法读取内容！");
            }
        } catch (IOException e) {
            log.error("读取文件内容时发生 IO 异常:", e);
        }
        return content.toString();
    }


    /**
     * 获取用于比较文件顺序的比较器。
     *
     * @return 比较器
     */
    private static Comparator<Path> getComparator() {
        return (p1, p2) -> {
            String fileName1 = p1.getFileName().toString();
            String fileName2 = p2.getFileName().toString();
            int index1 = fileName1.lastIndexOf('-') + 1;
            int index2 = fileName2.lastIndexOf('-') + 1;
            String number1 = fileName1.substring(index1);
            String number2 = fileName2.substring(index2);
            return Integer.compare(Integer.parseInt(number1), Integer.parseInt(number2));
        };
    }

    /**
     * 方法说明：校验Multipart文件的文件类型是否满足预期
     *
     * @param file      上传的文件
     * @param fileType  预期的文件类型
     * @author ZhuYin
     * @since 2025年02月09日
     */
    public static void validExtension(MultipartFile file, BaseEnums.FileType fileType) {
        if (fileType.equals(BaseEnums.FileType.excel)) {
            String originalFilename = Optional.ofNullable(file.getOriginalFilename()).orElse("");
            String fileExtension = FileUtil.getFileExtension(originalFilename);
            String xls = "xls";
            String xlsx = "xlsx";
            if (!xls.equalsIgnoreCase(fileExtension) && !xlsx.equalsIgnoreCase(fileExtension)) {
                throw new MyException("请上传xls或xlsx格式的文件");
            }
        }
    }

}
